﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using Hiro.ActivationPoints.Interfaces;
using Hiro.Interfaces;
using LinFu.Reflection.Emit;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Hiro.Containers;

namespace Hiro.ActivationPoints
{
    /// <summary>
    /// Represents the activation point used for property injection.
    /// </summary>
    public class PropertyActivationPoint : IPropertyActivationPoint
    {
        /// <summary>
        /// The target property.
        /// </summary>
        private readonly PropertyInfo _targetProperty;

        /// <summary>
        /// The target property resolver that will be used to determine the dependencies that will be used
        /// to inject the target property.
        /// </summary>
        private readonly IPropertyResolver _resolver;

        /// <summary>
        /// Initializes a new instance of the PropertyActivationPoint class.
        /// </summary>
        /// <param name="targetProperty">The property that will be used for dependency injection.</param>
        /// <param name="resolver">The property dependency resolver.</param>
        public PropertyActivationPoint(PropertyInfo targetProperty, IPropertyResolver resolver)
        {
            _targetProperty = targetProperty;
            _resolver = resolver;
        }

        /// <summary>
        /// Gets the value indicating the target type that holds the target property.
        /// </summary>
        /// <value>The declaring type of the target property.</value>
        public Type TargetType
        {
            get { return _targetProperty.DeclaringType; }
        }

        /// <summary>
        /// Gets the value indicating the property that will be used for property injection.
        /// </summary>
        /// <value>The <see cref="PropertyInfo"/> object that describes the target property.</value>
        public PropertyInfo TargetProperty
        {
            get { return _targetProperty; }
        }

        /// <summary>
        /// Determines whether or not the current <see cref="IDependencyMap"/> has the necessary
        /// dependencies for the current activation point to emit the necessary IL to inject the current property.
        /// </summary>
        /// <param name="dependencyContainer">The target dependency container.</param>
        /// <returns><c>true</c> if the property can be injected; otherwise, it will return <c>false</c>.</returns>
        public bool IsValidWith(IDependencyContainer dependencyContainer)
        {
            // This activation point can only work if and only if 
            // the dependency can be resolved
            var dependency = _resolver.Resolve(_targetProperty, dependencyContainer);
            if (dependency == null || !dependencyContainer.Contains(dependency))
                return false;

            // HACK: Prevent a stack overflow exception by skipping the property injection
            // call if the declaring type of the setter method is the same as type that
            // will be injected into the target property
            var dependencyMap = (IDependencyMap)dependencyContainer;
            var typeActivationPoint = dependencyMap.GetActivationPoint(dependency) as ITypeActivationPoint;

            var setterMethod = _targetProperty.GetSetMethod();
            if (typeActivationPoint != null && setterMethod.DeclaringType == typeActivationPoint.TargetType)
                return false;

            return true;
        }

        /// <summary>
        /// Emits the necessary <paramref name="IL"/> instructions to inject the current property.
        /// </summary>
        /// <param name="targetMethod">The method that will perform the property injection.</param>
        /// <param name="IL">The <see cref="CilWorker"/> that points to the method body of the method that will do the property injection.</param>
        /// <param name="hostModule">The module that contains the container type.</param>
        /// <param name="dependencyMap">The dependency map that describes the list of dependencies in the current application.</param>
        public void Emit(MethodReference targetMethod, CilWorker IL, ModuleDefinition hostModule, IDependencyMap dependencyMap)
        {
            var declaringType = targetMethod.DeclaringType;
            var module = hostModule;

            // Instantiate the target dependency
            var dependency = _resolver.Resolve(_targetProperty, dependencyMap);
            if (dependency == null)
                return;

            // Import the setter method
            var propertySetter = _targetProperty.GetSetMethod();
            var setterMethod = module.Import(propertySetter);
            var getInstanceMethod = module.ImportMethod<IMicroContainer>("GetInstance");

            var getTypeFromHandle = module.ImportMethod<Type>("GetTypeFromHandle", BindingFlags.Public | BindingFlags.Static);
            var serviceTypeRef = module.Import(dependency.ServiceType);
        
            var getTypeMethod = module.ImportMethod<object>("GetType");

            // NOTE: This assumes that the target type is already pushed onto the stack
            IL.Emit(OpCodes.Dup);

            // Push the MicroContainer onto the stack
            IL.Emit(OpCodes.Ldarg_0);

            // Push the service type onto the stack            
            IL.Emit(OpCodes.Ldtoken, serviceTypeRef);
            IL.Emit(OpCodes.Call, getTypeFromHandle);

            // Push the service name onto the stack
            var pushServiceName = dependency.ServiceName == null ? IL.Create(OpCodes.Ldnull) : IL.Create(OpCodes.Ldstr, dependency.ServiceName);
            IL.Append(pushServiceName);

            IL.Emit(OpCodes.Callvirt, getInstanceMethod);

            // Cast it to the property type
            var setterParameters = setterMethod.Parameters;
            var propertyType = setterParameters[0].ParameterType;
            IL.Emit(OpCodes.Isinst, propertyType);

            // Call the setter
            var callInstruction = propertySetter.IsVirtual ? IL.Create(OpCodes.Callvirt, setterMethod) : IL.Create(OpCodes.Call, setterMethod);
            IL.Append(callInstruction);
        }
    }
}
